#! /usr/bin/python
# -*- coding: utf-8 -*-
# @author HanYZ
# @createAt 2022.07.18
# 网易萧先生-神龙摆尾

import backtrader as bt

class StrategyClass(bt.Strategy):
    '''神龙摆尾'''
    params = dict(
        ma = 'S',          # 'S': SMA、'E':EMA、'W':WMA
        period_b1 = 22,    # 前箱体长度
        period_b2 = 5,     # 后箱体长度
        boxR = 7,          # 前箱体检测，60、22、10 三均线最大差值放大boxR倍仍不高于股价
        breakdown = 2,     # 后箱体接受几次下突破
        needvcd = False,   # 需要量均死叉
        maxuplimit = -1,   # 后箱体等待期涨停上限，负数表示不限制
        postboxV = 1,      # 后箱体规则，1：公开课版本; 2: 正课版本，3：自动切换
        closeup1 = 1.05,   # 目标止盈
        closeup2 = 1.1,    # 顶部回落止盈
        closedown = 0.76,  # 止损
        code = None,
        name = None,
        log = True,
    )

    def log(self, txt, dt=None, force=False):
        if force or self.p.log:
            dt = dt or self.data.datetime.datetime()
            who = f'{self.p.code} {self.p.name} ' if self.p.code else ''
            print(f'{dt.isoformat()} {who}{txt}')

    def notify_order(self, order):
        if order.status in [bt.Order.Submitted, bt.Order.Accepted]:
            return

        if order.status == order.Completed:
            if order.isbuy():
                buytxt = '买入价:%.2f, 量:%s, 持仓:%s' % (order.executed.price, order.executed.size, self.position.size)
                self.log(buytxt, bt.num2date(order.executed.dt))
            else:
                selltxt = '卖出价, %.2f, 量:%s, 持仓:%s' % (
                    order.executed.price, order.executed.size, self.position.size)
                if 'log' in order.info:
                    selltxt = '%s %s' % (selltxt, order.info.log)
                self.log(selltxt, bt.num2date(order.executed.dt))

        elif order.status in [order.Expired, order.Canceled, order.Margin]:
            if 'log' not in order.info:
                self.log('%s , %s' % (order.Status[order.status], order))
            pass

    def __init__(self):
        ma = bt.ind.EMA if self.p.ma == 'E' else bt.ind.WMA if self.p.ma == 'W' else bt.ind.SMA

        vma5 = ma(self.data.volume, period=5)
        vma10 = ma(self.data.volume, period=10)
        ma10 = ma(self.data, period=10)
        ma22 = ma(self.data, period=22)
        ma60 = ma(self.data, period=60)

        self.xxx = (ma10, ma22, ma60)
        self.yyy = bt.Max(abs(ma60 - ma22), abs(ma60 - ma10), abs(ma22 - ma10))
        self.zzz = bt.Max(abs(ma60 - ma22), abs(ma60 - ma10), abs(ma22 - ma10)) * self.p.boxR

        # 涨停
        self.limitup = (self.data.close(-1) * 1.1 - 0.01 <= self.data.close)
        # # 前箱体最高
        # self.preBoxHigh = bt.ind.Highest(self.data.high, period=self.p.period_b1)
        # 横盘-均线贴近
        self.preBox = bt.Max(abs(ma60 - ma22), abs(ma60 - ma10), abs(ma22 - ma10)) * self.p.boxR < self.data
        # 后箱体上沿
        self.boxHigh = bt.ind.Highest(self.data, period=self.p.period_b2)
        # 后箱体下沿
        self.boxLow = bt.ind.Lowest(self.data, period=self.p.period_b2)
        # 后箱体均线交叉
        self.m5c10 = bt.ind.CrossOver(vma5, vma10)

        self.signal = None # [涨停柱后静默期，下突破线，下突破计数，死叉，上破线]
        self.order = None

    def onLimitup(self):
        self.signal = [
            len(self) + self.p.period_b2,   # 0 静默期
            self.data.close[0],             # 1 箱体下突破线
            self.p.breakdown,               # 2 下突破计数
            False,                          # 3 死叉
            self.data.close[0],             # 4 上突破线
            None,                           # 5 买单
            None,                           # 6 止赢单
            self.data.close[-1],            # 7 箱体V1下线
            0,                              # 8 后箱体涨停计数
        ]

    def checkBreakup(self):
        curr = len(self)
        close = self.data.close[0]
        s = self.signal

        if self.limitup[0]:
            s[8] += 1
        else:
            if close < s[7]:  # 向下突破
                self.log('跌穿V1箱体，重新等待涨停')
                self.signal = None
                return False
            if close < s[1]:  # 向下突破
                s[2] -= 1
                if s[2] < 0 and self.p.postboxV == 2:
                    self.log('跌穿箱体，重新等待涨停')
                    self.signal = None  # 重新来过
                    return False

        if not s[3]:      # 未见死叉
            s[3] = self.m5c10 < 0

        if curr == s[0]:  # 箱体长度达标
            v = self.p.postboxV
            if v == 3:
                v = 1 if s[2] < 0 else 2
            if v == 2:
                s[4] = self.boxHigh[0]
            else:
                s[4] = s[1]
                s[1] = s[7]
        elif curr > s[0] and close > s[4]:   # 箱后突破
            if s[3] or not self.p.needvcd:   # 已有死叉
                return True
            else:
                self.log('万事俱备，只欠死叉，继续等待')
                s[4] = close

        if self.p.maxuplimit >= 0 and s[8] > self.p.maxuplimit:
            self.log('后箱体涨停次数超限，取消观察')
            self.signal = None

        return False

    def next(self):
        if self.signal:
            if self.signal[6]:
                if self.signal[6].status in [bt.Order.Canceled, bt.Order.Completed]:
                    self.signal = None
                elif self.signal[6].status in [bt.Order.Expired, bt.Order.Margin]:
                    print('订单状态异常', self.signal[6].status)
                elif self.data.close[0] * self.p.closeup2 < self.boxHigh[0]:
                    self.sell(exectype=bt.Order.Market, log=' (高位回落)止盈 ', oco=self.signal[6])
            elif self.checkBreakup():
                self.log('向上突破箱体，买入')
                self.signal[5] = self.buy()
                self.signal[6] = self.sell(exectype=bt.Order.Limit, price=self.data.close[0] * self.p.closeup1, log=' 止盈 ')
                self.sell(exectype=bt.Order.Stop, price=self.signal[1], log=' 初始止损 ', oco=self.signal[6])
                if self.p.closedown > 0:
                    self.sell(exectype=bt.Order.Stop, price=self.data.close[0] * self.p.closedown, log=' 百分比止损', oco=self.signal[6])
        elif self.limitup[0]:
            if self.preBox[0]:
                self.log('横盘后涨停，开始观察')
                self.onLimitup()
            else:
                self.log('涨停，但横盘不足')
